Angular 團隊在企業應用程式的效能方面做出了許多改進。 組件中的 Change Detection 的預設值是ChangeDetection.Default。這意味著組件無論其狀態如何,總是運行 Change Detection。 當使用預設值時,未修改的元件必須不必要地執行 Change Detection。當應用程式具有大型元件樹 (component tree) 時,這可能會損害應用程式的效能,其中一個變更可能會觸發所有元件執行 Change Detection。
然後,團隊引入了 OnPush change strategy,減少了組件樹中組件之間的 change detection 次數。
@Component({
selector: 'app-on-push-grand-child',
template: `...inline template…`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandChildComponent {}
當元件加入 changeDetection: ChangeDetectionStrategy.OnPush 時,它使用 OnPush change strategy 來執行 change detection。在以下情況下,元件將運行 change detection:
OnPush change strategy 優化了效能,但 Angular 17 引入了 local change detection (局部變化偵測),它只會更新訊號更新的元件,而忽略元件樹的其餘部分。
Change strategy 的預設值為 Default。當元件具有 Default change strategy 時,無論是否已修改,change detection 都會運作。我們舉個例子來解釋一下。
export function getCurrentTime(): string {
return new Date(Date.now()).toISOString();
}
@Component({
selector: 'app-default-grand-child',
template: `
<p>{{ showCurrentTime() }}</p>
<p>Counter: {{ count() }}</p>
<button (click)="add()">Add</button>`
})
export class DefaultGrandChildComponent {
count = signal(0);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
DefaultGrandChildComponent 元件具有 Default change strategy。它有一個增加 count 訊號的按鈕,showCurrentTime 方法顯示當前時間。
@Component({
selector: 'app-default-child',
imports: [DefaultGrandChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<p>Internal Count: {{ count() }}</p>
<button (click)="add()">Add</button>
<app-default-grand-child />`
})
export class DefaultChildComponent {
count = signal(0);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
DefaultChildComponent 元件也具有 Default change strategy,並且是 DefaultGrandChildComponent 元件的父元件。類似地,它有一個按鈕來增加 count 訊號和 showCurrentTime 來顯示當前時間。
@Component({
selector: 'app-root',
imports: [OnPushChildComponent, DefaultChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<button (click)="counterService.add()">Add CounterService value</button>
<app-on-push-child />
<app-on-push-child />
<app-default-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
counterService = inject(CounterService);
showCurrentTime() {
return getCurrentTime();
}
}
AppComponent 是 DefaultChildComponent 元件的父元件,它有 OnPush change strategy。
該元件被標記為 dirty。當偵測到變化時,此元件會增加 count 訊號並顯示當前時間。 DefaultChildComponent 元件是其父元件;因此,DefaultChildComponent 被標記為 dirty。 AppComponent 是 DefaultChildComponent 元件的父元件;因此,它被標記為 dirty。 OnPushChildComponent 的 change strategy 是 OnPush;它沒有接收新的輸入,event listener 沒有運行,沒有 AsyncPipe,也沒有更新訊號。因此,它不會被標記為 dirty,並且其子樹 (subtree) 會被忽略。
當 change detection 發生時,AppComponent、 DefaultChildComponent 和 DefaultGrandChildComponent 元件會更新。
類似地,DefaultChildComponent 和 AppComponent 元件也被標記為 dirty。 DefaultGrandChildComponent 具有 Default change strategy;因此,儘管其狀態沒有改變,但它被標記為 dirty。
當 change detection 發生時,AppComponent、DefaultChildComponent 和 DefaultGrandChildComponent 元件會更新。 Default change strategy 效能不佳,因為 DefaultChildComponent 的子樹 (subtree) 始終運行。 當其子樹增長時,應用程式會變得緩慢,因為所有元件都被標記為 dirty,並且更新所有元件。
@Injectable({
providedIn: 'root'
})
export class CounterService {
private readonly value = signal(0);
readValue = this.value.asReadonly();
add(delta=1) {
this.value.update((prev) => prev + delta);
}
}
CounterService 服務具有 add 方法增加的 value 訊號。 readValue 是 value 訊號的唯讀訊號。
@Component({
selector: 'app-on-push-grand-child',
template: `
<p>{{ showCurrentTime() }}</p>
<p>Internal Counter: {{ count() }}</p>
<p>Counter Service value: {{ counterService.readValue() }}</p>
<button (click)="add()">Add</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandChildComponent {
count = signal(0);
counterService = inject(CounterService);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
OnPushGrandChildComponent 元件具有 OnPush change strategy。它有一個增加 count 訊號的按鈕,showCurrentTime 方法顯示當前時間。此外,它還注入了 CounterService 來顯示 readValue 訊號的值。
@Component({
selector: 'app-on-push-child',
imports: [OnPushGrandChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<p>Internal Count: {{ count() }}</p>
<button (click)="add()">Add</button>
<app-on-push-grand-child />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushChildComponent {
count = signal(0);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
OnPushChildComponent 元件也具有 OnPush change strategy,並且是 OnPushGrandChildComponent 元件的父元件。類似地,它有一個按鈕來增加 count 訊號和 showCurrentTime 方法來顯示當前時間。
該元件被標記為 dirty。當偵測到變化時,此元件會增加 counter 訊號並顯示當前時間。 OnPushChildComponent 是它的父元件;因此,OnPushChildComponent 被標記為 dirty。 AppComponent 是 OnPushChildComponent 元件的父元件;因此,它被標記為 dirty。 DefaultChildComponent 的 change strategy 是 Default;它的子樹 (subtree) 總是被更新。
當 change detection 發生時,AppComponent、DefaultChildComponent、DefaultGrandChildComponent、OnPushChildComponent 和 OnPushGrandChildComponent 元件都會更新。
類似地,AppComponent 和 DefaultChildComponent 的子樹也被標記為 dirty。 OnPushGrandChildComponent 具有 OnPush change strategy,並且不符合任何 change detectio 標準。它沒有接收新的輸入,沒有運作 event listener,沒有 AsyncPipe,也沒有訊號更新;因此,它沒有被標記為 dirty。
當變更偵測發生時,AppComponent、DefaultChildComponent、DefaultGrandChildComponent 和 OnPushChildComponent 元件會更新。 OnPush change strategy 優化了應用程式的效能,因為當子樹 (subtree) 不符 change detection 標準時,它們不會執行 change detection。當子樹增長時,只有根和觸發事件的元件之間的元件才會被標記為 dirty 並更新。受影響組件的數量顯著減少。
在 Angular 17 中,團隊為訊號添加了 local change detection。訊號更新時,只有一個組件被標記為 dirty 並更新。
@Component({
selector: 'app-root',
imports: [OnPushChildComponent, DefaultChildComponent],
template: `
<p>{{ showCurrentTime() }}</p>
<button (click)="counterService.add()">Add CounterService value</button>
<div class="child" >
<app-on-push-child />
<app-on-push-child />
<app-default-child />
</div>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
counterService = inject(CounterService);
showCurrentTime() {
return getCurrentTime();
}
}
當 AppComponent 元件點擊按鈕時,它會增加 CounterService 的 value 訊號。 由於運行了 event listener,因此它被標記為 dirty。
@Component({
selector: 'app-on-push-grand-child',
template: `
<p>{{ showCurrentTime() }}</p>
<p>Counter Service value: {{ counterService.readValue() }}</p>
<button (click)="add()">Add</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushGrandChildComponent {
count = signal(0);
counterService = inject(CounterService);
add(delta=1) {
this.count.update((prev) => prev + delta);
}
showCurrentTime() {
return getCurrentTime();
}
}
OnPushGrandChildComponent 元件在模板中顯示 CounterService 的 value 訊號;因此將其標記為dirty。 它的父元件 OnPushChildComponent 並未因為 local change detection 而被標記為 dirty 元件。
假設 DefaultChildComponent 和 DefaultGrandChildComponent 不存在,則只更新 AppComponent 和 OnPushGrandChildComponent 元件。如果在 OnPushChildComponent 和 OnPushGrandChildComponent 之間插入更多元件,local change detection 將確保這些元件不會被標記為 dirty。 Change detection 的數量固定為三個;一個用於 AppComponent,另外兩個用於兩個 OnPushGrandChildComponent 元件。
我們應該從 local change detection 中獲益,並在現代 Angular 開發中使用訊號來實現 reactivity,